// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "generator/internal/client_generator.h"
#include "google/cloud/internal/absl_str_cat_quiet.h"
#include "absl/memory/memory.h"
#include "generator/internal/codegen_utils.h"
#include "generator/internal/descriptor_utils.h"
#include "generator/internal/predicate_utils.h"
#include "generator/internal/printer.h"
#include <google/api/client.pb.h>
#include <google/protobuf/descriptor.h>

namespace google {
namespace cloud {
namespace generator_internal {

ClientGenerator::ClientGenerator(
    google::protobuf::ServiceDescriptor const* service_descriptor,
    VarsDictionary service_vars,
    std::map<std::string, VarsDictionary> service_method_vars,
    google::protobuf::compiler::GeneratorContext* context)
    : ServiceCodeGenerator("client_header_path", "client_cc_path",
                           service_descriptor, std::move(service_vars),
                           std::move(service_method_vars), context) {
  // Remember if there are methods from google.iam.v1.GetIamPolicyRequest and
  // google.iam.v1.SetIamPolicyRequest to google.iam.v1.Policy with signature
  // extensions. If so, we'll generate a "set" wrapper method to help prevent
  // simultaneous updates of a policy from overwriting each other.
  for (google::protobuf::MethodDescriptor const& method : methods()) {
    auto const& method_signature_extension =
        method.options().GetRepeatedExtension(google::api::method_signature);
    if (method.output_type()->full_name() == "google.iam.v1.Policy") {
      auto const& input_type = method.input_type()->full_name();
      for (auto const& extension : method_signature_extension) {
        if (input_type == "google.iam.v1.GetIamPolicyRequest" &&
            extension == "resource") {
          get_iam_policy_extension_ = &method;
        }
        if (input_type == "google.iam.v1.SetIamPolicyRequest" &&
            extension == "resource,policy") {
          set_iam_policy_extension_ = &method;
        }
      }
    }
  }
}

Status ClientGenerator::GenerateHeader() {
  HeaderPrint(CopyrightLicenseFileHeader());
  HeaderPrint(  // clang-format off
    "// Generated by the Codegen C++ plugin.\n"
    "// If you make any local changes, they will be lost.\n"
    "// source: $proto_file_name$\n"
    "\n"
    "#ifndef $header_include_guard$\n"
    "#define $header_include_guard$\n"
    "\n");
  // clang-format on

  // includes
  HeaderLocalIncludes({vars("connection_header_path"), "google/cloud/future.h",
                       "google/cloud/options.h",
                       "google/cloud/polling_policy.h",
                       "google/cloud/status_or.h", "google/cloud/version.h"});
  if (get_iam_policy_extension_ && set_iam_policy_extension_) {
    HeaderLocalIncludes({"google/cloud/iam_updater.h"});
  }
  HeaderSystemIncludes(MethodSignatureWellKnownProtobufTypeIncludes());
  HeaderSystemIncludes(
      {HasLongrunningMethod() ? "google/longrunning/operations.grpc.pb.h" : "",
       HasMessageWithMapField() ? "map" : "", "memory"});
  HeaderPrint("\n");

  auto result = HeaderOpenNamespaces();
  if (!result.ok()) return result;

  // Client Class
  HeaderPrint(  // clang-format off
    "$class_comment_block$\n"
    "class $client_class_name$ {\n"
    " public:\n"
    "  explicit $client_class_name$(std::shared_ptr<$connection_class_name$> connection, Options options = {});\n"
    "  ~$client_class_name$();\n"
    "\n"
    "  //@{\n"
    "  // @name Copy and move support\n"
    "  $client_class_name$($client_class_name$ const&) = default;\n"
    "  $client_class_name$& operator=($client_class_name$ const&) = default;\n"
    "  $client_class_name$($client_class_name$&&) = default;\n"
    "  $client_class_name$& operator=($client_class_name$&&) = default;\n"
    "  //@}\n"
    "\n"
    "  //@{\n"
    "  // @name Equality\n"
    "  friend bool operator==($client_class_name$ const& a, $client_class_name$ const& b) {\n"
    "    return a.connection_ == b.connection_;\n"
    "  }\n"
    "  friend bool operator!=($client_class_name$ const& a, $client_class_name$ const& b) {\n"
    "    return !(a == b);\n"
    "  }\n"
    "  //@}\n"
    "\n");
  // clang-format on

  for (google::protobuf::MethodDescriptor const& method : methods()) {
    auto method_signature_extension =
        method.options().GetRepeatedExtension(google::api::method_signature);
    for (int i = 0; i < method_signature_extension.size(); ++i) {
      std::string method_string =
          absl::StrCat("  $method_name$($method_signature", i,
                       "$, Options options = {});\n\n");
      HeaderPrintMethod(
          method,
          {MethodPattern(
               {
                   {FormatMethodCommentsFromRpcComments(
                       method, MethodParameterStyle::kApiMethodSignature)},
                   {IsResponseTypeEmpty,
                    // clang-format off
                   "  Status\n",
                   "  StatusOr<$response_type$>\n"},
                  {method_string}
                   // clang-format on
               },
               All(IsNonStreaming, Not(IsLongrunningOperation),
                   Not(IsPaginated))),
           MethodPattern(
               {
                   {FormatMethodCommentsFromRpcComments(
                       method, MethodParameterStyle::kApiMethodSignature)},
                   {IsResponseTypeEmpty,
                    // clang-format off
                    "  future<Status>\n",
                    "  future<StatusOr<$longrunning_deduced_response_type$>>\n"},
                   {method_string}
                   // clang-format on
               },
               All(IsNonStreaming, IsLongrunningOperation, Not(IsPaginated))),
           MethodPattern(
               {
                   // clang-format off
                   {FormatMethodCommentsFromRpcComments(
                     method, MethodParameterStyle::kApiMethodSignature)},
                   {"  StreamRange<$range_output_type$>\n"},
                   {method_string},
                   // clang-format on
               },
               All(IsNonStreaming, Not(IsLongrunningOperation), IsPaginated)),
           MethodPattern(
               {
                   // clang-format off
                   {FormatMethodCommentsFromRpcComments(
                     method, MethodParameterStyle::kApiMethodSignature)},
                   {"  StreamRange<$response_type$>\n"},
                   {method_string},
                   // clang-format on
               },
               IsStreamingRead)},
          __FILE__, __LINE__);

      if (get_iam_policy_extension_ && set_iam_policy_extension_ == &method) {
        auto response_type = ProtoNameToCppName(
            set_iam_policy_extension_->output_type()->full_name());
        auto set_method_name = set_iam_policy_extension_->name();
        auto get_method_name = get_iam_policy_extension_->name();
        HeaderPrint({
            PredicatedFragment<void>(""),
            {R"""(  /**
   * Updates the IAM policy for @p resource using an optimistic concurrency
   * control loop.
   *
   * The loop fetches the current policy for @p resource, and passes it to @p
   * updater, which should return the new policy. This new policy should use the
   * current etag so that the read-modify-write cycle can detect races and rerun
   * the update when there is a mismatch. If the new policy does not have an
   * etag, the existing policy will be blindly overwritten. If @p updater does
   * not yield a policy, the control loop is terminated and kCancelled is
   * returned.
   *
   * @param resource  Required. The resource for which the policy is being
   * specified. See the operation documentation for the appropriate value for
   * this field.
   * @param updater  Required. Functor to map the current policy to a new one.
   * @param options  Optional. Options to control the loop. Expected options
   * are:
   *       - `$service_name$BackoffPolicyOption`
)"""},
            {"   * @return " + response_type + "\n"},
            {"   */\n"},
            {"  StatusOr<" + response_type + ">\n"},
            {"  " + set_method_name},
            {"(std::string const& resource,"
             " IamUpdater const& updater,"
             " Options options = {});\n\n"},
        });
      }
    }
  }

  for (google::protobuf::MethodDescriptor const& method : async_methods()) {
    auto method_signature_extension =
        method.options().GetRepeatedExtension(google::api::method_signature);
    for (int i = 0; i < method_signature_extension.size(); ++i) {
      std::string method_string =
          absl::StrCat("  Async$method_name$($method_signature", i,
                       "$, Options options = {});\n\n");
      HeaderPrintMethod(
          method,
          {MethodPattern(
              {
                  {FormatMethodCommentsFromRpcComments(
                      method, MethodParameterStyle::kApiMethodSignature)},
                  {IsResponseTypeEmpty,
                   // clang-format off
                   "  future<Status>\n",
                   "  future<StatusOr<$response_type$>>\n"},
                  {method_string}
                  // clang-format on
              },
              All(IsNonStreaming, Not(IsLongrunningOperation),
                  Not(IsPaginated)))},
          __FILE__, __LINE__);
    }
  }

  for (auto const& method : methods()) {
    HeaderPrintMethod(
        method,
        {MethodPattern(
             {
                 {FormatMethodCommentsFromRpcComments(
                     method, MethodParameterStyle::kProtobufRequest)},
                 {IsResponseTypeEmpty,
                  // clang-format off
    "  Status\n",
    "  StatusOr<$response_type$>\n"},
   {"  $method_name$($request_type$ const& request, Options options = {});\n"
        "\n"}
                 // clang-format on
             },
             All(IsNonStreaming, Not(IsLongrunningOperation),
                 Not(IsPaginated))),
         MethodPattern(
             {
                 {FormatMethodCommentsFromRpcComments(
                     method, MethodParameterStyle::kProtobufRequest)},
                 {IsResponseTypeEmpty,
                  // clang-format off
    "  future<Status>\n",
    "  future<StatusOr<$longrunning_deduced_response_type$>>\n"},
   {"  $method_name$($request_type$ const& request, Options options = {});\n"
        "\n"}
                 // clang-format on
             },
             All(IsNonStreaming, IsLongrunningOperation, Not(IsPaginated))),
         MethodPattern(
             {
                 // clang-format off
                 {FormatMethodCommentsFromRpcComments(
        method, MethodParameterStyle::kProtobufRequest)},
   {"  StreamRange<$range_output_type$>\n"
    "  $method_name$($request_type$ request, Options options = {});\n\n"},
                 // clang-format on
             },
             All(IsNonStreaming, Not(IsLongrunningOperation), IsPaginated)),
         MethodPattern(
             {
                 // clang-format off
                 {FormatMethodCommentsFromRpcComments(
        method, MethodParameterStyle::kProtobufRequest)},
   {"  StreamRange<$response_type$>\n"
    "  $method_name$($request_type$ const& request, Options options = {});\n\n"},
                 // clang-format on
             },
             IsStreamingRead)},
        __FILE__, __LINE__);
  }

  for (auto const& method : async_methods()) {
    HeaderPrintMethod(
        method,
        {MethodPattern(
            {
                {FormatMethodCommentsFromRpcComments(
                    method, MethodParameterStyle::kProtobufRequest)},
                {IsResponseTypeEmpty,
                 // clang-format off
    "  future<Status>\n",
    "  future<StatusOr<$response_type$>>\n"},
   {"  Async$method_name$($request_type$ const& request, Options options = {});\n"
        "\n"}
                // clang-format on
            },
            All(IsNonStreaming, Not(IsLongrunningOperation),
                Not(IsPaginated)))},
        __FILE__, __LINE__);
  }

  HeaderPrint(  // clang-format off
    " private:\n"
    "  std::shared_ptr<$connection_class_name$> connection_;\n"
    "  Options options_;\n");
  // clang-format on

  // close Client class
  HeaderPrint(  // clang-format off
    "};\n\n");
  // clang-format on

  HeaderCloseNamespaces();
  // close header guard
  HeaderPrint(  // clang-format off
    "#endif  // $header_include_guard$\n");
  // clang-format on
  return {};
}

Status ClientGenerator::GenerateCc() {
  CcPrint(CopyrightLicenseFileHeader());
  CcPrint(  // clang-format off
    "// Generated by the Codegen C++ plugin.\n"
    "// If you make any local changes, they will be lost.\n"
    "// source: $proto_file_name$\n\n");
  // clang-format on

  // includes
  CcLocalIncludes(
      {vars("client_header_path"), vars("option_defaults_header_path")});
  CcSystemIncludes({"memory"});
  if (get_iam_policy_extension_ && set_iam_policy_extension_) {
    CcLocalIncludes({vars("options_header_path")});
    CcSystemIncludes({"thread"});
  }
  CcPrint("\n");

  auto result = CcOpenNamespaces();
  if (!result.ok()) return result;

  CcPrint(  // clang-format off
    "$client_class_name$::$client_class_name$(std::shared_ptr<$connection_class_name$> connection, Options options)"
    " : connection_(std::move(connection)),"
    " options_($product_internal_namespace$::$service_name$DefaultOptions(std::move(options))) {}\n");
  // clang-format on

  CcPrint(  // clang-format off
    "$client_class_name$::~$client_class_name$() = default;\n\n");
  // clang-format on

  for (google::protobuf::MethodDescriptor const& method : methods()) {
    auto method_signature_extension =
        method.options().GetRepeatedExtension(google::api::method_signature);
    for (int i = 0; i < method_signature_extension.size(); ++i) {
      std::string method_string =
          absl::StrCat("$client_class_name$::$method_name$($method_signature",
                       i, "$, Options options) {\n");
      std::string method_request_string =
          absl::StrCat("$method_request_setters", i, "$");
      CcPrintMethod(
          method,
          {MethodPattern(
               {
                   {IsResponseTypeEmpty,
                    // clang-format off
                   "Status\n",
                   "StatusOr<$response_type$>\n"},
                  {method_string},
                  {"  internal::OptionsSpan span(internal::MergeOptions("
                   "std::move(options), options_));\n"},
                  {"  $request_type$ request;\n"},
                   {method_request_string},
                  {"  return connection_->$method_name$(request);\n"
                   "}\n\n"}
                   // clang-format on
               },
               All(IsNonStreaming, Not(IsLongrunningOperation),
                   Not(IsPaginated))),
           MethodPattern(
               {
                   {IsResponseTypeEmpty,
                    // clang-format off
                    "future<Status>\n",
                    "future<StatusOr<$longrunning_deduced_response_type$>>\n"},
                  {method_string},
                  {"  internal::OptionsSpan span(internal::MergeOptions("
                   "std::move(options), options_));\n"},
                  {"  $request_type$ request;\n"},
                   {method_request_string},
                  {"  return connection_->$method_name$(request);\n"
                  "}\n\n"}
                   // clang-format on
               },
               All(IsNonStreaming, IsLongrunningOperation, Not(IsPaginated))),
           MethodPattern(
               {
                   // clang-format off
                   {"StreamRange<$range_output_type$>\n"},
                  {method_string},
                  {"  internal::OptionsSpan span(internal::MergeOptions("
                   "std::move(options), options_));\n"},
                  {"  $request_type$ request;\n"},
                   {method_request_string},
                  {"  return connection_->$method_name$(request);\n"
                  "}\n\n"}
                   // clang-format on
               },
               All(IsNonStreaming, Not(IsLongrunningOperation), IsPaginated)),
           MethodPattern(
               {
                   // clang-format off
                   {"StreamRange<$response_type$>\n"},
                  {method_string},
                  {"  internal::OptionsSpan span(internal::MergeOptions("
                   "std::move(options), options_));\n"},
                  {"  $request_type$ request;\n"},
                   {method_request_string},
                  {"  return connection_->$method_name$(request);\n"
                  "}\n\n"}
                   // clang-format on
               },
               IsStreamingRead)},
          __FILE__, __LINE__);

      if (get_iam_policy_extension_ && set_iam_policy_extension_ == &method) {
        auto response_type = ProtoNameToCppName(
            set_iam_policy_extension_->output_type()->full_name());
        auto set_method_name = set_iam_policy_extension_->name();
        auto get_method_name = get_iam_policy_extension_->name();
        CcPrint({
            PredicatedFragment<void>(""),
            {"StatusOr<" + response_type + ">\n"},
            {"$client_class_name$::" + set_method_name},
            {"(std::string const& resource,"
             " IamUpdater const& updater,"
             " Options options) {\n"
             "  internal::CheckExpectedOptions<$service_name$"
             "BackoffPolicyOption>(options, __func__);\n"
             "  internal::OptionsSpan span(internal::MergeOptions("
             "std::move(options), options_));\n"
             "  auto backoff_policy = internal::CurrentOptions()"
             ".get<$service_name$BackoffPolicyOption>()->clone();\n"
             "  for (;;) {\n"},
            {"    auto recent = " + get_method_name + "(resource);\n"},
            {"    if (!recent) {\n"
             "      return recent.status();\n"
             "    }\n"
             "    auto policy = updater(*std::move(recent));\n"
             "    if (!policy) {\n"
             "      return Status(StatusCode::kCancelled,"
             " \"updater did not yield a policy\");\n"
             "    }\n"},
            {"    auto result = " + set_method_name +
             "(resource, *std::move(policy));\n"},
            {"    if (result || result.status().code() != StatusCode::kAborted)"
             " {\n"
             "      return result;\n"
             "    }\n"
             "    std::this_thread::sleep_for("
             "backoff_policy->OnCompletion());\n"
             "  }\n"
             "}\n\n"},
        });
      }
    }
  }

  for (google::protobuf::MethodDescriptor const& method : async_methods()) {
    auto method_signature_extension =
        method.options().GetRepeatedExtension(google::api::method_signature);
    for (int i = 0; i < method_signature_extension.size(); ++i) {
      std::string method_string = absl::StrCat(
          "$client_class_name$::Async$method_name$($method_signature", i,
          "$, Options options) {\n");
      std::string method_request_string =
          absl::StrCat("$method_request_setters", i, "$");
      CcPrintMethod(
          method,
          {MethodPattern(
              {
                  {IsResponseTypeEmpty,
                   // clang-format off
                   "future<Status>\n",
                   "future<StatusOr<$response_type$>>\n"},
                  {method_string},
                  {"  internal::OptionsSpan span(internal::MergeOptions("
                   "std::move(options), options_));\n"},
                  {"  $request_type$ request;\n"},
                   {method_request_string},
                  {"  return connection_->Async$method_name$(request);\n"
                   "}\n\n"}
                  // clang-format on
              },
              All(IsNonStreaming, Not(IsLongrunningOperation),
                  Not(IsPaginated)))},
          __FILE__, __LINE__);
    }
  }

  for (auto const& method : methods()) {
    CcPrintMethod(
        method,
        {MethodPattern(
             {
                 {IsResponseTypeEmpty,
                  // clang-format off
    "Status\n",
    "StatusOr<$response_type$>\n"},
   {"$client_class_name$::$method_name$($request_type$ const& request"
    ", Options options) {\n"
    "  internal::OptionsSpan span(internal::MergeOptions("
    "std::move(options), options_));\n"
    "  return connection_->$method_name$(request);\n"
    "}\n\n"}
                 // clang-format on
             },
             All(IsNonStreaming, Not(IsLongrunningOperation),
                 Not(IsPaginated))),
         MethodPattern(
             {
                 {IsResponseTypeEmpty,
                  // clang-format off
    "future<Status>\n",
    "future<StatusOr<$longrunning_deduced_response_type$>>\n"},
   {"$client_class_name$::$method_name$($request_type$ const& request"
    ", Options options) {\n"
    "  internal::OptionsSpan span(internal::MergeOptions("
    "std::move(options), options_));\n"
    "  return connection_->$method_name$(request);\n"
    "}\n\n"}
                 // clang-format on
             },
             All(IsNonStreaming, IsLongrunningOperation, Not(IsPaginated))),
         MethodPattern(
             {
                 // clang-format off
   {"StreamRange<$range_output_type$>\n"
    "$client_class_name$::$method_name$($request_type$ request"
    ", Options options) {\n"
    "  internal::OptionsSpan span(internal::MergeOptions("
    "std::move(options), options_));\n"
    "  return connection_->$method_name$(std::move(request));\n"
    "}\n\n"}
                 // clang-format on
             },
             All(IsNonStreaming, Not(IsLongrunningOperation), IsPaginated)),
         MethodPattern(
             {
                 // clang-format off
   {"StreamRange<$response_type$>\n"
    "$client_class_name$::$method_name$($request_type$ const& request"
    ", Options options) {\n"
    "  internal::OptionsSpan span(internal::MergeOptions("
    "std::move(options), options_));\n"
    "  return connection_->$method_name$(request);\n"
    "}\n\n"}
                 // clang-format on
             },
             IsStreamingRead)},
        __FILE__, __LINE__);
  }

  for (auto const& method : async_methods()) {
    CcPrintMethod(
        method,
        {MethodPattern(
            {
                {IsResponseTypeEmpty,
                 // clang-format off
    "future<Status>\n",
    "future<StatusOr<$response_type$>>\n"},
   {"$client_class_name$::Async$method_name$($request_type$ const& request"
    ", Options options) {\n"
    "  internal::OptionsSpan span(internal::MergeOptions("
    "std::move(options), options_));\n"
    "  return connection_->Async$method_name$(request);\n"
    "}\n\n"}
                // clang-format on
            },
            All(IsNonStreaming, Not(IsLongrunningOperation),
                Not(IsPaginated)))},
        __FILE__, __LINE__);
  }

  CcCloseNamespaces();
  return {};
}

}  // namespace generator_internal
}  // namespace cloud
}  // namespace google
